Skip to content

fix: 六处「答案已记录、做决定的代码不读它」+ 明确拒绝 system 工具链 (2026.8.30.2) - #538

Merged
Sunrisepeak merged 5 commits into
mainfrom
feat/workspace-inheritance-and-record-durability
Aug 30, 2026
Merged

fix: 六处「答案已记录、做决定的代码不读它」+ 明确拒绝 system 工具链 (2026.8.30.2)#538
Sunrisepeak merged 5 commits into
mainfrom
feat/workspace-inheritance-and-record-durability

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 30, 2026

Copy link
Copy Markdown
Member

分析、量化与设计:.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md
每一条都有复现,数字都是实测的。

关闭 #529,修复 #527 的 Bug 1 / Bug 2 / RFC 2 / RFC 3。

host 依赖的规则按轴分,这个分叉是刻意的

归谁 答案
工具链 mcpp 的契约 拒绝import std 可用、闭包可计算、"同一份构建在别的机器和 CI 上一致",全都是关于一个 mcpp 解析得出、叫得出名字的编译器的陈述;PATH 上的编译器让这些全部无法核验。msvc@system唯一例外(它点名一个族,mcpp 定位其安装)
程序链接的库 程序自己的事 提示,不拒绝。工程可以链 host 的库或自己的 .so;mcpp 说明代价并指出 mcpp-index 那条路(索引没有就贡献一个),但只要能构建能运行就不拦。产物是开发者的,由他保证

库这条轴内部仍沿用旧判据:证明跑不起来才拒绝(现有的闭包检查不变)。

修复

1. mcpp test 每次调用都在重算一份没变的答案(#529)

两个 post-link ELF pass 都写了读回优化,而 prepare_build 每次调用都用一个全新的
json 对象
重写 resolution.json —— 里面没有这两条记录。每次调用一开始就删掉了自己
后端待会儿要找的备忘录。

  • 记录搬进 .mcpp-runtime-verdicts.json(它活得过 prepare_build),resolution.json
    继续发布副本 —— 这正是 sync_resolution_verdict 已有的形态;
  • 剪枝判据从「不在本次 plan 里」改为「产物已不在磁盘上」:buildtest 共用一个
    输出目录而 link unit 集合不同,前一个判据让两条命令互删对方的记录;
  • 失效键纳入 SubOS farm 的 .xlings.json 戳与 MCPP_ALLOW_HOST_LIBS —— 让备忘录持久化,
    就产生了一条以前不存在的正确性义务。

实测(10 个 link unit × 11.5MB,全热):

之前 之后
mcpp build -p <member> 0.70s 0.32s
mcpp test -p <member>(紧接 build) 3.15s 0.36s
mcpp test -p <member>(连续) 1.95s 0.40s

2. path 依赖里新增源文件,fast path 看不见(没有人报过)

$ printf 'export module dep.third;\n...' > ../dep/src/third.cppm
$ mcpp build
    Finished dev in 0.00s        # 那个模块从没编译过,而它报了成功

内容改动能被抓到不是靠扫描,是靠 ninja 重链后的事后放弃。这是 #359 那条形态在它当年
没覆盖到的目录里 —— workspace 成员之间就是 path 依赖,所以不是边角情况。

3. [toolchain] system 现在被明确拒绝(#527 Bug 1)

它此前配合 build.mcpp 会死在 posix_spawnp('') failed (error 2)
一条以崩溃形式出现的"拒绝"不是政策,是穿着政策外衣的 bug。

拒绝在读取工具链声明处触发(早于任何编译),消息给出该写什么、去哪看可选项,并点明
msvc@system 例外与「host 库是另一条轴」。

⚠️ 三个既有测试引用了这个逃生口,其中两个原本会继续「通过」:

测试 发生了什么 怎么改的
14_toolchain_fallback 断言只有一条否定(不出现 "no toolchain configured"),而任何别的错误都满足它 —— 于是它在自己的意图反转之后继续绿 改为正反两面都断言
293_…_name_one_os system 把 Linux 编译器指向 Windows 目标;拒绝先触发,于是它开始走 skip 分支 —— 而它自己的注释写着「skip 必须挣来,否则测试看不见回退」 把拒绝作为一条带理由的 PASS 分支:它守的那道门现在整个关上了
105_asm_sources_nasm 确实不受影响(坏 MCPP_HOME 的 bootstrap 错误仍先触发) 不改;核实过,不是假设

4. standard = 26(不带引号)被静默忽略

get_string 对裸整数返回空 ⇒ 按默认档位编译、零诊断。#527 自己的三处示例就是这么写的。

5. [workspace.package] / [workspace.build](#527 Bug 2 + RFC 3)

标量按「成员声明过就成员优先」继承,向量按 workspace 在前追加。「声明过」是解析时
记录的事实
,不是与默认值比较的推断 —— 成员刻意写 standard = "c++23" 必须保住,而那与
默认值同为一串字节。这正是 cpp20 设计文档 §9-Q3 记下并推迟的前置条件。

allow_host_libs 明确不可继承;不认识的键拒绝而不是忽略。

6. 依赖声明了高于当前图的 standard 时会说出来

degraded,--strict 提升,只对工程作者自己拥有的 manifest 生效:索引里带 mcpp 段的
描述符 782/782 全都声明了 language,其中 756/774 是 import_std = false 的 C 库带的
样板值。

7. 方言标志没进 import std 预编译时,编译前拒绝

生效后的标志集合;图中没有 import std 时不触发;只对根包生效(依赖带着该标志却
不 import std 是合法的)。-fno-exceptions / -fno-rtti 仍然不自动提升

测试

tests/e2e/321..326,每条都带让检查保持诚实的否定用例与分母。
本机 96 unit 全过;e2e 全量跑过(唯一红的 168已发布的 mcpp 跑同样红 —— 本机
musl 工具链是 gcc 13.3.0,无 std module,与本 PR 无关)。

…2026.8.30.2)

Analysis, measurements and design:
`.agents/docs/2026-08-30-issues-527-529-535-537-analysis-and-design.md`.

Two families. A record exists and the code that decides does not read it; or
the runtime search path mixes what was declared with what this machine happens
to have installed. The host-dependence policy that settles every severity here
is stated once in that document's §1.2 and in docs/03: mcpp and everything the
ecosystem publishes depend on no host, a user's own project may and that choice
is theirs to guarantee, and the boundary between a warning and a refusal is
whether the result builds and runs.

`mcpp test` re-derived an unchanged answer on every invocation (#529). Both
post-link ELF passes were written with a read-back keyed on the artifact's
stat, and `prepare_build` rewrites `resolution.json` from a fresh json object
at the start of every run, carrying neither record — so each invocation began
by deleting the memo its own backend was about to look for. The records move to
`.mcpp-runtime-verdicts.json`, which survives, and `resolution.json` keeps
publishing a copy, which is the shape `sync_resolution_verdict` already had.
Pruning now asks whether the artifact left the DISK rather than whether it left
the current command's plan: `build` and `test` share one output directory with
different link-unit sets and were deleting each other's verdicts. The record's
invalidation key gains the SubOS farm stamp and MCPP_ALLOW_HOST_LIBS, because
making a memo durable creates a staleness obligation that did not exist while
the answer was recomputed. Measured on ten link units, all warm: `mcpp test`
after a `mcpp build` 3.15s -> 0.36s.

A new source file in a `path` dependency was invisible to the fast path, which
swept only the project being built. `mcpp build` printed `Finished dev in
0.00s` and the module was never compiled. Content edits were caught, but by the
post-link snapshot rather than by the sweep. This is #359's shape in a
directory that fix did not reach, and workspace members depend on each other by
`path`, so it is not an edge case. The dependency source roots are recorded in
`.build_cache` and swept, manifest included.

`[toolchain] system` with a `build.mcpp` died as `posix_spawnp('') failed`
(#527 Bug 1). The resolved compiler path was in `tc->binaryPath` the whole time
and was not handed to the build.mcpp closure. This fills an unset variable; it
adds no host capability, and the same manifest without a build.mcpp already
built. The host-dependence warning states the cost once per build and names the
xim route.

`standard = 26` — the spelling #527 uses in three of its own examples — was
silently ignored, because the key is documented as a string and `get_string`
returns nothing for a bare integer. Both spellings are now accepted.

`[workspace.package]` and `[workspace.build]` (#527 Bug 2, RFC 3). The
workspace root's `[build]` reached no member. Scalars are inherited when the
member did not DECLARE the key, vectors append workspace-first, and
"declared" is recorded by both parse paths rather than inferred by comparing
against the default — a member pinning `standard = "c++23"` under a c++26
workspace must keep it, and that is the same bytes as the default. This is the
precondition the cpp20 design doc's §9-Q3 wrote down and deferred. Both
inheritance sites now call one function. `allow_host_libs` is refused there:
it turns a correctness gate off, and a root able to set it once would disable
it for members added later by someone who never read that file.

A dependency declaring a standard above the graph's is now reported instead of
silently discarded, degraded and promoted by --strict, and scoped to manifests
the author controls: every index descriptor with an mcpp segment declares
`language` (782 of 782 measured locally, 756 of 774 being C libraries carrying
a boilerplate "c++23"), so declaredness does not mean authorship there.

A dialect-class flag in `cxxflags` that never reaches the `import std` prebuild
is refused before compiling, naming `dialect_cxxflags`. Read from the effective
flag set, so a profile or target block is covered too; silent when nothing in
the graph imports std, and scoped to the root package because a dependency
carrying the flag may legitimately not import std at all.

Tests: tests/e2e/321..326, each with the negative case that keeps the check
honest. Docs: 03, 05, 06 in both languages.
The host-dependence rule is not uniform across axes, and the split is the
point rather than an inconsistency.

THE TOOLCHAIN IS MCPP'S OWN CONTRACT. Everything mcpp promises — that
`import std` is available, that the runtime closure is computable, that two
machines and CI produce the same build — is a statement about a compiler mcpp
resolved and can identify. A compiler taken from PATH makes every one of those
unverifiable, so `[toolchain] … = "system"` is refused rather than warned
about. `msvc@system` is the single exception and is a different spelling: it
names a FAMILY whose installation mcpp locates, on the one platform where the
compiler cannot be redistributed.

THE LIBRARIES A PROGRAM LINKS ARE THE PROGRAM'S BUSINESS. A project may link a
host library or its own `.so`; mcpp names the supported route — declare the
provider so it resolves from mcpp-index, contribute the package if the index
does not carry it yet — and does not refuse while the result builds and runs.
The developer owns the artifact and guarantees it.

This replaces the previous commit's treatment of #527 Bug 1, which filled in
the resolved compiler path and warned. The crash it removed was real —
`posix_spawnp('') failed (error 2)` as soon as the project had a build.mcpp —
but a refusal that arrives as a crash three layers down is not a policy, it is
a bug wearing one. The refusal now fires during toolchain resolution, before
anything tries to compile the build program, and says what to write instead.

Three existing tests referenced the escape hatch and each needed a different
answer:

  14_toolchain_fallback  asserted only that `system` did NOT produce "no
                         toolchain configured". That predicate stays satisfied
                         by any other error, so the test went on passing while
                         its stated intent inverted — a negative-only
                         assertion cannot tell "it worked" from "it failed
                         differently". Both halves are checked now.

  293_…_name_one_os      used `system` to point a Linux compiler at a Windows
                         target. The refusal fires first, so the test began
                         taking its skip branch — and its own header says a
                         skip there has to be earned or the test cannot see a
                         revert. The refusal is now an accepted PASS branch
                         with its own reason, because the invariant holds by a
                         stronger mechanism: that door is closed entirely.

  105_asm_sources_nasm   genuinely unaffected; its broken-MCPP_HOME bootstrap
                         error still fires first. Verified, not assumed.

325 is rewritten accordingly, and asserts the refusal reaches the user before
the build program starts, that it fires for the environment side channel too,
that it names the msvc@system exception and the library axis, and — the
denominator — that a project with no `[toolchain]` at all still builds.

`mcpp.diag`'s host-route helper is reverted: with the toolchain axis refusing
rather than warning, and the library-provenance work not in this change, it
had no consumer. Shipping an unread field is the defect this branch is about.

Also fixes the version constant: `modules/versioning/src/version.cppm` is the
second source of truth `check_version_pins.sh` enforces, and CI caught it —
that mismatch is what failed e2e on all three platforms and the Windows
`SubsystemContracts.TheBinaryVersionMatchesTheRootManifest`.
…reference doc

The design document argued a single boundary — "does it build and run" — for
every host dependency, and D15 followed it to "warn, do not refuse". §1.2 is
corrected to state the rule per axis: the toolchain is mcpp's contract and is
refused, the libraries a program links are the program's own and stay a
warning. §7, §9, §10, §11.7, §12 and the review record follow.

The measurement behind the earlier conclusion was right and stays in the
document — `[toolchain] system` does build a project using `import std`. What
was wrong was carrying it across an axis boundary, which is the failure this
document keeps finding from the other side.

Also drops a second-person sentence from docs/06 that check_docs_style.sh
refuses in a reference doc.
@Sunrisepeak Sunrisepeak changed the title fix: 六处「答案已记录、做决定的代码不读它」(2026.8.30.2) fix: 六处「答案已记录、做决定的代码不读它」+ 明确拒绝 system 工具链 (2026.8.30.2) Aug 30, 2026
Found by re-reading the merge, not by a failure. `[workspace.build]
include_dirs = ["shared/inc"]` was prepended to each member verbatim, so every
member resolved it against its OWN directory — looking for
`<member>/shared/inc` for a directory that lives at `<workspace>/shared/inc`.

This is #224 for a new key. `[indices].path` and `[workspace.dependencies]
path` are anchored to the workspace root for exactly this reason, and a third
relative-path key that skipped it fails as a missing header three members deep,
naming neither the manifest that declared it nor the root it was written
against. Anchored rather than refused: `expandIncludeDirs` already accepts an
absolute include directory, so the anchored form needs nothing downstream.

321 now includes a header from the workspace root, so the anchoring has an
assertion rather than a comment.
The ecosystem job runs this file and checks that each test "ran to its
conclusion" — an assertion that exists so an early exit cannot masquerade as a
pass. My branch for the toolchain refusal did `exit 0`, which skipped half two
entirely: four correct cross builds that this file also guards. CI caught it;
a local run did not, because locally the exit code is all a caller sees.

The branch now records that half one is settled and lets the script continue.
The "names both systems" assertion is asked only of the OS-mismatch refusal —
the toolchain refusal is a different sentence about a different decision, one
that never resolved a target at all, and demanding both triples from it would
be asserting on the wrong object.
@Sunrisepeak
Sunrisepeak merged commit 7735387 into main Aug 30, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants